#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <unordered_map>
#include <mutex>
#include "util.hpp"
#include "log.hpp"

namespace ns_index
{
    // 存储正排索引内容
    struct DocInfo
    {
        std::string title;   // 文档的标题(去标签化后)
        std::string content; // 文档的内容(去标签化后)
        std::string url;     // 官方文档的url
        uint64_t doc_id;     // 文档的ID
    };

    // 存储倒排索引内容
    struct InvertedElem
    {
        int doc_id;       // 关键字对应的文档ID
        std::string word; // 关键字
        int weight;       // 该文档ID所占权重
    };

    // 倒排拉链
    typedef std::vector<InvertedElem> InvertedList;

    // 索引类(设置为单例模式)
    class Index
    {
    private:
        // 正排索引的数据结构用数组，数组的下标就是文档的ID
        std::vector<DocInfo> forward_index; // 正排索引
        // 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
        std::unordered_map<std::string, InvertedList> inverted_index; // 倒排索引
        
        std::unordered_map<std::string, int> advertise_rec; // 记录竞价信息
    private:
        // 设置为单例模式
        Index() {}
        Index(const Index &) = delete;
        Index &operator=(const Index &) = delete;

        static Index *instance;
        static std::mutex mtx;

    public:
        ~Index() {}

    public:
        // 获取Index的单例实例
        static Index *GetInstance()
        {
            // 双重检查锁避免多线程环境下对单例实例的重复创建
            // 如果只有一次检查加锁, 可能会出现多个线程同时检查到单例实例为nullptr, 然后同时进入临界区创建单例实例, 导致单例实例被创建多次
            if (nullptr == instance)
            {
                mtx.lock();
                if (nullptr == instance)
                {
                    instance = new Index();
                }
                mtx.unlock();
            }

            return instance;
        }

        // 根据doc_id找到文档内容
        DocInfo *GetForwardIndex(uint64_t doc_id)
        {
            // 查找的doc_id大于正排索引的总长度, 报错
            if (doc_id >= forward_index.size())
            {
                LogMessage(FATAL, "seek doc_id out range");
                return nullptr;
            } 

            // 返回值为指针
            return &forward_index[doc_id];
        }

        // 根据倒排索引哈希表中的关键字word获得倒排拉链
        InvertedList *GetInvertedList(const std::string &word)
        {
            // 查找word关键字映射的倒排拉链
            auto iter = inverted_index.find(word);
            // 没有找到
            if (iter == inverted_index.end())
            {
                LogMessage(WARNING, "not seek inverted_index");
                return nullptr;
            }
            
            // 返回word对应的倒排拉链
            return &(iter->second);
        }

        // 返回竞价信息哈希表
        std::unordered_map<std::string, int> GetAdvertise_rec()
        {
            return advertise_rec;
        }

        // 根据去标签化和格式化后的文档, 构建正排索引和倒排索引
        bool BuildIndex(const std::string &input)
        {
            // // 以输入模式(只读)打开file_path, 并按照二进制方式进行读取
            std::ifstream in(input, std::ios::in | std::ios::binary);
            if (!in.is_open())
            {
                LogMessage(FATAL, "open %s error", input.c_str());
                return false;
            }

            std::string line;
            int count = 0;
            while (std::getline(in, line))
            {
                // 根据每一行(一个文件: 前面用\n作为文件分隔符)构建正排索引
                DocInfo *doc = BuildForwardIndex(line);
                if (nullptr == doc)
                {
                    LogMessage(WARNING, "build ForwardIndex:  %s error", line.c_str());
                    continue;
                }

                // 根据每个正排索引的内容构建倒排索引
                BuildInvertedIndex(*doc);
                
                // 每有一个文件, ++count
                ++count;
                // 每有50个文件, 输出调试信息
                if(count % 50 == 0)
                {
                    LogMessage(INFO, "当前已经建立的索引文档: %d", count);
                }
            }

            return true;
        }

        // 添加竞价排名信息
        bool AddAdvertise()
        {
            const std::string Ad = "data/Ad/advertise.txt";

            // 将广告信息读取到advertise中
            std::string advertise;
            // 以输入模式(只读)打开file_path
            std::ifstream in(Ad, std::ios::in);
            // 文件没有打开成功
            if(!in.is_open())
            {
                LogMessage(WARNING, "open file %s error", Ad.c_str());
                return false;
            }

            // line用于读取in输入文件流的一行文本
            std::string line;
            while(std::getline(in, line))
            {
                // 提取url
                std::string url = line.substr(0, line.find('|'));
                // 提取出价
                int price = atoi(line.substr(line.find('|') + strlen("\3")).c_str());
                // 插入advertise数组中
                advertise_rec[url] = price;
            }

            return true;
        }

    private:
        DocInfo *BuildForwardIndex(const std::string &line)
        {
            // 1. 解析line, 进行字符串切分
            // line -> 3string: title, content, url
            std::vector<std::string> results;
            std::string sep = "\3"; // 文件信息分隔符
            // 将切分后的3个信息存入results中
            ns_util::StringUtil::Split(line, &results, sep);
            // 切分的数据不为3个, 报错
            if (results.size() != 3)
            {
                LogMessage(FATAL, "%s Segmentation error", line.c_str());
                return nullptr;
            }
            // 2. 将3个字符串填充到DocInfo
            DocInfo doc;
            doc.title = results[0];
            doc.content = results[1];
            doc.url = results[2];
            // doc_id就是forward_index的下标, 因为下标从0开始, doc_id正好是当前的size
            doc.doc_id = forward_index.size();
            // 3. 插入到正排索引的vector中
            forward_index.push_back(std::move(doc));

            return &forward_index.back();
        }

        bool BuildInvertedIndex(const DocInfo &doc)
        {    
            // 记录标题与内容中关键字出现的次数, 即词频统计
            struct word_cnt
            {
                int title_cnt;
                int content_cnt;

                word_cnt() : title_cnt(0), content_cnt(0) {}
            };
            std::unordered_map<std::string, word_cnt> word_map; // 用来暂存词频的映射表

            // 对标题进行分词
            std::vector<std::string> title_words;
            ns_util::JiebaUtil::CutString(doc.title, &title_words);

            // 对标题进行词频统计
            for (auto s : title_words)
            {
                boost::to_lower(s);      // 将分词统一转化为小写
                word_map[s].title_cnt++;
            }

            // 对内容进行分词
            std::vector<std::string> content_words;
            ns_util::JiebaUtil::CutString(doc.content, &content_words);

            // 对内容进行词频统计
            for (auto s : content_words)
            {
                boost::to_lower(s); // 将分词统一转化为小写
                word_map[s].content_cnt++;
            }

// 规定标题和内容出现一次所占权重, X和Y的值根据相关性的不同设置不同
// X代表标题, Y代表内容
#define X 5
#define Y 1
            // 遍历暂存词频的映射表, 构建倒排索引的哈希表
            for (auto &word_pair : word_map)
            {
                // 构建倒排索引的内容
                InvertedElem item;
                item.doc_id = doc.doc_id;
                item.word = word_pair.first;
                
                item.weight = X * word_pair.second.title_cnt + Y * word_pair.second.content_cnt;
                // 根据关键字在其对应的倒排拉链处插入倒排索引, 构建倒排索引哈希表
                inverted_index[word_pair.first].push_back(std::move(item));
            }

            return true;
        }
    };
    // 定义两个静态成员变量, 用于实现单例模式
    Index *Index::instance = nullptr;
    std::mutex Index::mtx;
}